EKS オーケストレータを使った SageMaker HyperPod クラスターで FSx for Lustre をマウントしてみた
こんにちは!クラウド事業本部コンサルティング部のたかくに(@takakuni_)です。
みなさん SageMaker HyperPod 使っていますか?
今回は EKS オーケストレータを利用した SageMaker HyperPod で FSx for Lustre ファイルシステムをマウントする機会があったため、ブログにしてみたいと思います。
Slurm オーケストレータ版を探している場合は以下をご覧ください。
FSx for Lustre
AWS が提供する高性能なフルマネージドな分散並列ファイルシステムです。
FSx for Lustre を利用することで 1/100 秒未満のレイテンシ、最大数百 GBps のスループット、最大数百万の IOPS の性能を有したファイルシステムを利用できます。
データリポジトリ
FSx for Lustre では S3 と統合しており、 S3 オブジェクトをファイルとして透過的に書き込み/読み込みできます。
学習や推論で利用されるデータはサイズが大きいことが多々あり、各ノードが起動するタイミングで直接 S3 からダウンロードするのは時間もお金も大きくかかります。
FSx for Lustre を中継させることで、コスト効率を高める効果が見込めます。またチェックポイントを共有するファイルストレージとしても非常に有用です。
通信要件
EKS オーケストレータの場合、 EKS クラスターセキュリティグループと HyperPod ノードで利用するセキュリティグループを意識する必要がありますが、 Lustre ファイルシステムと疎通は HyperPod ノードのセキュリティグループのみ意識しておけば OK です。具体的な通信要件は EFA の有無で異なります。
EFA 無し
EFA なしの場合は SageMaker HyperPod ノードおよびノード自身のセキュリティグループを 988, 1018-1023 ポートで許可するように設定します。
Lustre ファイルシステム側
インバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
Custom TCP | TCP | 988 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 988 | SageMaker HyperPod ノードのセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | SageMaker HyperPod ノードのセキュリティグループ ID |
アウトバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
Custom TCP | TCP | 988 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 988 | SageMaker HyperPod ノードのセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | SageMaker HyperPod ノードのセキュリティグループ ID |
Lustre クライアント側
インバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
Custom TCP | TCP | 988 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 988 | Lustre ファイルシステムのセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | Lustre ファイルシステムのセキュリティグループ ID |
アウトバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
Custom TCP | TCP | 988 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 988 | Lustre ファイルシステムのセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | 自身のセキュリティグループ ID |
Custom TCP | TCP | 1018-1023 | Lustre ファイルシステムのセキュリティグループ ID |
すべてのトラフィック | すべて | すべて | 0.0.0.0/0 |
※一番最後のすべてのトラフィックは外部接続用のルールのため、 Lustre とのやりとりとは別のルールです。
EFA 有り
EFA 有りの場合は、すべての送受信トラフィックを許可する必要があります。
If you are going to create an EFA-enabled FSx for Lustre, you should first create an EFA-enabled security group and specify it as the security group for the file system. An EFA requires a security group that allows all inbound and outbound traffic to and from the security group itself and the security group of the clients if clients reside in a different security group. For more information, see Step 1: Prepare an EFA-enabled security group in the Amazon EC2 User Guide.
要約すると以下のルールを追加する必要があります。
Lustre ファイルシステム側
インバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
すべてのトラフィック | すべて | すべて | 自身のセキュリティグループ ID |
すべてのトラフィック | すべて | すべて | SageMaker HyperPod ノードのセキュリティグループ ID |
アウトバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
すべてのトラフィック | すべて | すべて | 自身のセキュリティグループ ID |
すべてのトラフィック | すべて | すべて | SageMaker HyperPod ノードのセキュリティグループ ID |
Lustre クライアント側
インバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
すべてのトラフィック | すべて | すべて | 自身のセキュリティグループ ID |
すべてのトラフィック | すべて | すべて | Lustre ファイルシステムのセキュリティグループ ID |
アウトバウンドルール
タイプ | プロトコル | ポート | ソース |
---|---|---|---|
すべてのトラフィック | すべて | すべて | 自身のセキュリティグループ ID |
すべてのトラフィック | すべて | すべて | Lustre ファイルシステムのセキュリティグループ ID |
すべてのトラフィック | すべて | すべて | 0.0.0.0/0 |
一番最後のすべてのトラフィックは外部接続用のルールのため、 Lustre とのやりとりとは別のルールです。
0.0.0.0/0
で許可をしていますが、合わせてセキュリティグループ ID も許可してあげる必要があります。
If you want to create a HyperPod cluster with EFA-enabled instances, make sure that you set up a security group to allow all inbound and outbound traffic to and from the security group itself. Note that allowing outbound traffic to 0.0.0.0/0 isn't sufficient and can cause EFA health checks to fail. Make sure that you add an explicit outbound traffic rule to the security group so that the instances in the security group can communicate. To learn more, see Step 1: Prepare an EFA-enabled security group in the Amazon EC2 User Guide.
SageMaker HyperPod 上で動かす際の注意点
FSx for Lustre は EKS 上の Pod に直接マウントはできないため、 Amazon FSx for Lustre CSI Driver を利用したボリュームのプロビジョニングが必要になります。
Amazon FSx for Lustre CSI Driver では自動的に Lustre ファイルシステムから永続ボリューム要求(Persistent Volume Claim)まで作成してくれる Dynamic Provisioning と、手動で作成する Static Provisioning があります。
今回は具体的にイメージがつきやすい Static Provisioning を行います。
やってみる
今回は再現性があるよう HashiCorp Terraform を利用して作成しました。各ステップで何をどのように作成したのか触れていきたいと思います。
一連のコードは以下に格納しています。
基本的な EKS オーケストレータと SageMaker HyperPod の部分は省略します。(以下のエントリに記載しているので興味があれば合わせてご覧ください。)
再掲になりますが、構成図は次の通りで EKS オーケストレータで動かす HyperPod クラスターがあり、インスタンスグループ(worker-group)の Pod に FSx for Lustre をマウントするような構成にしてみます。
Lustre ファイルシステム
まずはファイルシステムの準備です。S3 に透過的にアクセスしたいため、aws_fsx_data_repository_association
を利用してデータリポジトリを作成します。
###################################################
# Lustre File System
###################################################
resource "aws_fsx_lustre_file_system" "this" {
storage_type = "SSD"
file_system_type_version = "2.15"
storage_capacity = 1200
security_group_ids = [aws_security_group.lustre.id]
subnet_ids = [module.vpc.private_subnets[0]]
data_compression_type = "LZ4"
deployment_type = "PERSISTENT_2"
per_unit_storage_throughput = 250
metadata_configuration {
mode = "AUTOMATIC"
}
}
resource "aws_fsx_data_repository_association" "this" {
file_system_id = aws_fsx_lustre_file_system.this.id
data_repository_path = "s3://${aws_s3_bucket.data_repository.bucket}"
file_system_path = "/"
s3 {
auto_export_policy {
events = ["NEW", "CHANGED", "DELETED"]
}
auto_import_policy {
events = ["NEW", "CHANGED", "DELETED"]
}
}
}
EFA を利用しないですがセキュリティグループは Lustre, HyperPod Node どちらもフルオープンで許可を行いました。
###################################################
# Security Group for Lustre File System
###################################################
resource "aws_security_group" "lustre" {
name = "${local.prefix}-lustre-sg"
vpc_id = module.vpc.vpc_id
description = "${local.prefix}-hyperpod-sg"
tags = {
Name = "${local.prefix}-lustre-sg"
}
}
# Ingress
resource "aws_vpc_security_group_ingress_rule" "lustre_all_traffic_self" {
security_group_id = aws_security_group.lustre.id
referenced_security_group_id = aws_security_group.lustre.id
ip_protocol = "-1"
}
resource "aws_vpc_security_group_ingress_rule" "lustre_all_traffic_hyperpod" {
security_group_id = aws_security_group.lustre.id
referenced_security_group_id = aws_security_group.hyperpod.id
ip_protocol = "-1"
}
# Egress
resource "aws_vpc_security_group_egress_rule" "lustre_all_traffic_self" {
security_group_id = aws_security_group.lustre.id
referenced_security_group_id = aws_security_group.lustre.id
ip_protocol = "-1"
}
resource "aws_vpc_security_group_egress_rule" "lustre_all_traffic_hyperpod" {
security_group_id = aws_security_group.lustre.id
referenced_security_group_id = aws_security_group.hyperpod.id
ip_protocol = "-1"
}
###################################################
# Security Group for SageMaker HyperPod Cluster
###################################################
resource "aws_security_group" "hyperpod" {
name = "${local.prefix}-hyperpod-sg"
vpc_id = module.vpc.vpc_id
description = "${local.prefix}-hyperpod-sg"
tags = {
Name = "${local.prefix}-hyperpod-sg"
}
}
resource "aws_vpc_security_group_ingress_rule" "hyperpod_allow_all_traffic_ipv4" {
security_group_id = aws_security_group.hyperpod.id
referenced_security_group_id = aws_security_group.hyperpod.id
ip_protocol = "-1"
}
resource "aws_vpc_security_group_ingress_rule" "hyperpod_allow_all_traffic_eks" {
security_group_id = aws_security_group.hyperpod.id
referenced_security_group_id = aws_security_group.eks.id
ip_protocol = "-1"
}
resource "aws_vpc_security_group_egress_rule" "hyperpod_allow_all_traffic_ipv4" {
security_group_id = aws_security_group.hyperpod.id
cidr_ipv4 = "0.0.0.0/0"
ip_protocol = "-1"
}
resource "aws_vpc_security_group_egress_rule" "hyperpod_allow_all_traffic_self" {
security_group_id = aws_security_group.hyperpod.id
referenced_security_group_id = aws_security_group.hyperpod.id
ip_protocol = "-1"
}
Amazon FSx for Lustre CSI Driver
続いて Amazon FSx for Lustre CSI Driver のセットアップです。
Amazon FSx for Lustre CSI Driver は IRSA の仕組みで認証認可を行うため、OpenID Connect を利用した IAM ロールを作成します。
###################################################
# FSx CSI Driver
###################################################
data "tls_certificate" "this" {
url = aws_eks_cluster.this.identity[0].oidc[0].issuer
depends_on = [
aws_eks_cluster.this
]
}
resource "aws_iam_openid_connect_provider" "this" {
url = aws_eks_cluster.this.identity[0].oidc[0].issuer
client_id_list = [
"sts.amazonaws.com",
]
thumbprint_list = [
data.tls_certificate.this.certificates[0].sha1_fingerprint
]
}
data "aws_iam_policy_document" "assume_csi_driver" {
statement {
actions = [
"sts:AssumeRoleWithWebIdentity",
]
effect = "Allow"
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.this.arn]
}
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.this.url}:aud"
values = [
"sts.amazonaws.com",
]
}
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.this.url}:sub"
values = [
"system:serviceaccount:kube-system:fsx-csi-controller-sa",
]
}
}
}
resource "aws_iam_role" "fsx_csi_driver" {
name = "${local.prefix}-fsx-csi-driver-role"
assume_role_policy = data.aws_iam_policy_document.assume_csi_driver.json
}
resource "aws_iam_role_policy_attachment" "fsx_csi_driver" {
role = aws_iam_role.fsx_csi_driver.name
policy_arn = "arn:aws:iam::aws:policy/AmazonFSxFullAccess"
}
OpenID Connect を利用した IAM ロールのセットアップが済んだら、Amazon FSx for Lustre CSI Driver を helm 経由でインストールします。
インストール後、helm チャートでデプロイしたサービスアカウント(fsx-csi-controller-sa
)のアノテーションに先ほど作成した IAM ロールを紐付けます。
resource "helm_release" "aws_fsx_csi_driver" {
name = "aws-fsx-csi-driver"
repository = "https://kubernetes-sigs.github.io/aws-fsx-csi-driver"
chart = "aws-fsx-csi-driver"
namespace = "kube-system"
dependency_update = true
wait = false
depends_on = [
aws_eks_access_policy_association.this
]
}
data "kubernetes_service_account_v1" "this" {
metadata {
name = "fsx-csi-controller-sa"
namespace = "kube-system"
}
depends_on = [
helm_release.aws_fsx_csi_driver,
aws_eks_access_policy_association.this
]
}
resource "kubernetes_annotations" "this" {
api_version = "v1"
kind = "ServiceAccount"
metadata {
name = data.kubernetes_service_account_v1.this.metadata[0].name
namespace = data.kubernetes_service_account_v1.this.metadata[0].namespace
}
annotations = {
"eks.amazonaws.com/role-arn" = aws_iam_role.fsx_csi_driver.arn
}
depends_on = [
aws_eks_access_policy_association.this
]
}
Persistent Volume
最後に Stroage Class, Persistent Volume, Persistent Volume Claim を作成します。これで Kubernetes 側の設定は完了です。
###################################################
# FSx CSI Driver
###################################################
data "tls_certificate" "this" {
url = aws_eks_cluster.this.identity[0].oidc[0].issuer
depends_on = [
aws_eks_cluster.this
]
}
resource "aws_iam_openid_connect_provider" "this" {
url = aws_eks_cluster.this.identity[0].oidc[0].issuer
client_id_list = [
"sts.amazonaws.com",
]
thumbprint_list = [
data.tls_certificate.this.certificates[0].sha1_fingerprint
]
}
data "aws_iam_policy_document" "assume_csi_driver" {
statement {
actions = [
"sts:AssumeRoleWithWebIdentity",
]
effect = "Allow"
principals {
type = "Federated"
identifiers = [aws_iam_openid_connect_provider.this.arn]
}
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.this.url}:aud"
values = [
"sts.amazonaws.com",
]
}
condition {
test = "StringEquals"
variable = "${aws_iam_openid_connect_provider.this.url}:sub"
values = [
"system:serviceaccount:kube-system:fsx-csi-controller-sa",
]
}
}
}
resource "aws_iam_role" "fsx_csi_driver" {
name = "${local.prefix}-fsx-csi-driver-role"
assume_role_policy = data.aws_iam_policy_document.assume_csi_driver.json
}
resource "aws_iam_role_policy_attachment" "fsx_csi_driver" {
role = aws_iam_role.fsx_csi_driver.name
policy_arn = "arn:aws:iam::aws:policy/AmazonFSxFullAccess"
}
resource "helm_release" "aws_fsx_csi_driver" {
name = "aws-fsx-csi-driver"
repository = "https://kubernetes-sigs.github.io/aws-fsx-csi-driver"
chart = "aws-fsx-csi-driver"
namespace = "kube-system"
dependency_update = true
wait = false
depends_on = [
aws_eks_access_policy_association.this
]
}
data "kubernetes_service_account_v1" "this" {
metadata {
name = "fsx-csi-controller-sa"
namespace = "kube-system"
}
depends_on = [
helm_release.aws_fsx_csi_driver,
aws_eks_access_policy_association.this
]
}
resource "kubernetes_annotations" "this" {
api_version = "v1"
kind = "ServiceAccount"
metadata {
name = data.kubernetes_service_account_v1.this.metadata[0].name
namespace = data.kubernetes_service_account_v1.this.metadata[0].namespace
}
annotations = {
"eks.amazonaws.com/role-arn" = aws_iam_role.fsx_csi_driver.arn
}
depends_on = [
aws_eks_access_policy_association.this
]
}
###################################################
# Persistent Volume and Persistent Volume Claim
###################################################
resource "kubernetes_storage_class_v1" "this" {
metadata {
name = "fsx-sc"
}
storage_provisioner = "fsx.csi.aws.com"
parameters = {
"fileSystemId" = aws_fsx_lustre_file_system.this.id
"subnetId" = aws_fsx_lustre_file_system.this.subnet_ids[0]
"securityGroupIds" = aws_security_group.lustre.id
}
depends_on = [
aws_eks_access_policy_association.this
]
}
resource "kubernetes_persistent_volume_v1" "this" {
metadata {
name = "fsx-pv"
}
spec {
capacity = {
storage = "1200Gi"
}
access_modes = ["ReadWriteMany"]
volume_mode = "Filesystem"
persistent_volume_reclaim_policy = "Retain"
storage_class_name = kubernetes_storage_class_v1.this.metadata[0].name
persistent_volume_source {
csi {
driver = "fsx.csi.aws.com"
volume_handle = aws_fsx_lustre_file_system.this.id
volume_attributes = {
dnsname = aws_fsx_lustre_file_system.this.dns_name
mountname = aws_fsx_lustre_file_system.this.mount_name
}
}
}
}
depends_on = [
aws_eks_access_policy_association.this
]
}
resource "kubernetes_persistent_volume_claim_v1" "this" {
metadata {
name = "fsx-claim"
}
spec {
access_modes = ["ReadWriteMany"]
storage_class_name = kubernetes_storage_class_v1.this.metadata[0].name
resources {
requests = {
storage = "1200Gi"
}
}
}
depends_on = [
aws_eks_access_policy_association.this,
kubernetes_persistent_volume_v1.this
]
}
補足
PV の設定ですが Workshop の内容だと volumeAttributes の dnsname
と mountname
が指定されていません。
cat <<EOF> pv.yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: fsx-pv
spec:
capacity:
storage: 1200Gi # Adjust based on your FSx volume size
volumeMode: Filesystem
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
storageClassName: fsx-sc
csi:
driver: fsx.csi.aws.com
volumeHandle: fs-xxxxx # Replace with your FSx file system ID
EOF
kubectl apply -f pv.yaml
この状態だとマウントできず、次のエラーが発生します。
MountVolume.SetUp failed for volume "fsx-pv" : rpc error: code = InvalidArgument desc = dnsname is not provided
そのため、 aws-fsx-csi-driver の例を参考に dnsname
と mountname
を指定しました。
動作確認
動作確認用にサンプルファイルを S3 にアップロードします。
###################################################
# Hello Sample file
###################################################
resource "aws_s3_object" "hello_from_s3" {
bucket = aws_s3_bucket.data_repository.bucket
key = "hello_from_s3.txt"
content = "hello! from S3!"
depends_on = [aws_fsx_data_repository_association.this]
}
また、先ほど作成したボリュームをマウントするように Pod を作成します。Pod からも Lustre にファイルを書き込むような動作を行います。
############################################
# Sample Kubernetes Pod
############################################
resource "kubernetes_pod_v1" "this" {
metadata {
name = "fsx-app"
}
spec {
container {
name = "app"
image = "centos"
command = ["/bin/sh"]
args = ["-c", "while true; do echo $(date -u) >> /data/out.txt; sleep 5; done"]
volume_mount {
name = "persistent-storage"
mount_path = "/data"
}
}
volume {
name = "persistent-storage"
persistent_volume_claim {
claim_name = kubernetes_persistent_volume_claim_v1.this.metadata[0].name
}
}
}
depends_on = [
kubernetes_persistent_volume_v1.this,
awscc_sagemaker_cluster.this
]
}
Pod が作成後、 kubectl exec
で Pod 内に bash ログインしてみます。
/data
配下に 1.2 TB の領域が確認できますね。
takakuni@ sagemaker_hyperpod_lustre_eks % kubectl exec -it fsx-app -- /bin/bash
[root@fsx-app /]# df -h
Filesystem Size Used Avail Use% Mounted on
overlay 500G 6.8G 494G 2% /
tmpfs 64M 0 64M 0% /dev
tmpfs 7.8G 0 7.8G 0% /sys/fs/cgroup
10.0.1.159@tcp:/jm2njbev 1.2T 7.5M 1.2T 1% /data
/dev/nvme0n1p1 100G 20G 81G 20% /etc/hosts
/dev/nvme1n1 500G 6.8G 494G 2% /etc/hostname
shm 64M 0 64M 0% /dev/shm
tmpfs 15G 12K 15G 1% /run/secrets/kubernetes.io/serviceaccount
tmpfs 7.8G 0 7.8G 0% /proc/acpi
tmpfs 7.8G 0 7.8G 0% /sys/firmware
また、サンプルファイルとしてアップロードしたファイルの中身も確認できました。
[root@fsx-app /]# cat /data/
hello_from_s3.txt out.txt
[root@fsx-app /]# cat /data/hello_from_s3.txt
hello! from S3![root@fsx-app /]#
S3 側にも Pod によって作成されたデータが格納されていました。
まとめ
以上、「EKS オーケストレータを使った SageMaker HyperPod クラスターで FSx for Lustre をマウントしてみた」でした。
ほとんど「Amazon FSx for Lustre CSI Driver 使ってみた」な内容でしたが、参考になれば幸いです。
クラウド事業本部コンサルティング部のたかくに(@takakuni_)でした!